package kube

import (
	"bytes"
	"fmt"
	"io"
	"os"
	"strings"
	"text/template"

	"github.com/pkg/errors"
	bkeaddon "gopkg.openfuyao.cn/bkecommon/cluster/addon"
	templateutil "gopkg.openfuyao.cn/bkecommon/template"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/api/meta"
	apierrors2 "k8s.io/apimachinery/pkg/api/meta"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/apimachinery/pkg/util/uuid"
	yamlutil "k8s.io/apimachinery/pkg/util/yaml"
	"k8s.io/client-go/dynamic"
	"k8s.io/client-go/restmapper"

	"gopkg.openfuyao.cn/cluster-api-provider-bke/utils/addonutil"
)

var trueVar = true

func (c *Client) ApplyYaml(task *Task) error {
	c.Log.Infof("*****start %s yaml file %s*****", task.Operate, task.FilePath)
	defer c.Log.Infof("*****end %s yaml file %s*****", task.Operate, task.FilePath)
	defer func() {
		if err := recover(); err != nil {
			c.Log.Errorf("%s yaml file %s panic: %v", task.Operate, task.FilePath, err)
		}
	}()

	decoder, err := RenderYamlToDecoder(task)
	if err != nil {
		c.Log.Errorf("failed to render yaml %q: %v", task.FilePath, err)
		return errors.Wrapf(err, "failed to render yaml %q", task.FilePath)
	}
	dc := c.ClientSet.Discovery()
	restMapperRes, err := restmapper.GetAPIGroupResources(dc)
	if err != nil {
		return err
	}
	restMapper := restmapper.NewDiscoveryRESTMapper(restMapperRes)

	unstructList, err := GetUnStructListFromDecoder(decoder)
	if err != nil {
		c.Log.Errorf("failed to get unstruct list from file %s: %v", task.FilePath, err)
		return errors.Errorf("failed to get unstruct list from decoder: %v", err)
	}
	finalUnstructuredList := []unstructured.Unstructured{}
	for _, unstruct := range unstructList {
		if addonutil.IsListKind(unstruct.GetKind()) {
			list, err := addonutil.UnwrapList(*unstruct)
			if err != nil {
				c.Log.Errorf("failed to unwrap list Kind: %v", err)
				return errors.Errorf("failed to unwrap list Kind: %v", err)
			}
			finalUnstructuredList = append(finalUnstructuredList, list...)
			continue
		}
		finalUnstructuredList = append(finalUnstructuredList, *unstruct)
	}

	if task.Operate == bkeaddon.CreateAddon || task.Operate == bkeaddon.UpdateAddon || task.Operate == bkeaddon.UpgradeAddon {
		finalUnstructuredList = addonutil.SortInstallUnstructuredByKind(finalUnstructuredList)
	} else {
		finalUnstructuredList = addonutil.SortUninstallUnstructuredByKind(finalUnstructuredList)
	}

	for _, unstruct := range finalUnstructuredList {
		gvk := unstruct.GroupVersionKind()
		c.Log.Debugf("gvk: %v", gvk.String())
		mapping, err := restMapper.RESTMapping(gvk.GroupKind(), gvk.Version)
		if err != nil {
			// if no resource match, try refresh restMapper
			if apierrors2.IsNoMatchError(err) {
				dc = c.ClientSet.Discovery()
				restMapperRes, err = restmapper.GetAPIGroupResources(dc)
				if err != nil {
					return err
				}
				restMapper = restmapper.NewDiscoveryRESTMapper(restMapperRes)
			}
			// try again
			mapping, err = restMapper.RESTMapping(gvk.GroupKind(), gvk.Version)
			if err != nil {
				if apierrors2.IsNoMatchError(err) && gvk.Kind == "ServiceMonitor" {
					c.Log.Infof("addon obj Kind： %s, Name %s, APIVersion %s, not support %s in target cluster skip", unstruct.GetKind(), unstruct.GetName(), unstruct.GetAPIVersion(), task.Operate)
					continue
				}
				return err
			}
		}

		var dr dynamic.ResourceInterface
		if mapping.Scope.Name() == meta.RESTScopeNameNamespace {
			dr = c.DynamicClient.Resource(mapping.Resource).Namespace(unstruct.GetNamespace())
		} else {
			dr = c.DynamicClient.Resource(mapping.Resource)
		}

		// dry run first
		// dry run failed, skip this addon,if not provide failed continue
		//if !task.IgnoreError {
		//	_, err = c.DynamicClient.Resource(mapping.Resource).Namespace(unstruct.GetNamespace()).Create(c.Ctx, unstruct, metav1.CreateOptions{DryRun: []string{metav1.DryRunAll}})
		//	if err != nil {
		//		if apierrors.IsAlreadyExists(err) {
		//			c.Log.Warnf("addon obj Kind： %s, Name %s  already exists, skip create", unstruct.GetKind(), unstruct.GetName())
		//			continue
		//		}
		//		c.Log.Errorf("faild apply %v",gvk)
		//		return err
		//	}
		//}
		// create

		var obj *unstructured.Unstructured
		switch task.Operate {
		case bkeaddon.CreateAddon:
			//obj, err = dr.Create(c.Ctx, &unstruct, metav1.CreateOptions{})
			//if err != nil {
			//	if apierrors.IsAlreadyExists(err) {
			//		c.Log.Warnf("addon obj Kind： %s, Name %s  already exists, skip create", unstruct.GetKind(), unstruct.GetName())
			//		continue
			//	}
			//	// for svc(type NodePort)
			//	if apierrors.IsInvalid(err) {
			//		if status := apierrors.APIStatus(nil); errors.As(err, &status) {
			//			s := status.Status()
			//			if s.Details.Kind == unstruct.GetKind() && s.Details.Name == unstruct.GetName() && s.Details.Kind == "Service" {
			//				c.Log.Warnf("%s/%s already exists", unstruct.GetKind(), unstruct.GetName())
			//				continue
			//			}
			//		}
			//	}
			//	c.Log.Errorf("faild apply %v", gvk)
			//	return err
			//}
			obj, err = dr.Apply(c.Ctx, unstruct.GetName(), &unstruct, metav1.ApplyOptions{Force: true, FieldManager: "bke"})
			if err != nil {
				c.Log.Errorf("faild apply %v", gvk)
				return err
			}
			if task.recorder != nil {
				task.recorder.Record(obj)
			}

		// update only for addon params change,so skip CustomResourceDefinition, it's always static in the same version
		case bkeaddon.UpdateAddon:
			if gvk.Kind == "CustomResourceDefinition" {
				c.Log.Infof("skip update CustomResourceDefinition %s", unstruct.GetName())
				continue
			}
			json, err := unstruct.MarshalJSON()
			if err != nil {
				return err
			}
			obj, err = dr.Patch(c.Ctx, unstruct.GetName(), types.ApplyPatchType, json, metav1.PatchOptions{Force: &trueVar, FieldManager: "bke"})
			if err != nil {
				if apierrors.IsNotFound(err) {
					c.Log.Warnf("addon obj Kind： %s, Name %s  not found, skip update", unstruct.GetKind(), unstruct.GetName())
					continue
				}
				c.Log.Errorf("faild update %v", gvk)
				return err
			}
			if task.recorder != nil {
				task.recorder.Record(obj)
			}
		case bkeaddon.UpgradeAddon:
			obj, err = dr.Apply(c.Ctx, unstruct.GetName(), &unstruct, metav1.ApplyOptions{Force: true, FieldManager: "bke"})
			//if err != nil {
			//	return err
			//}
			//obj, err = dr.Patch(c.Ctx, unstruct.GetName(), types.ApplyPatchType, json, metav1.PatchOptions{Force: &trueVar})
			if err != nil {
				//if apierrors.IsAlreadyExists(err) {
				//	// update
				//	obj, err = dr.Update(c.Ctx, &unstruct, metav1.UpdateOptions{})
				//	if err != nil {
				//		c.Log.Errorf("faild update(in upgrade process) %v", gvk)
				//		return err
				//	}
				//} else {
				//	c.Log.Errorf("faild upgrade %v", gvk)
				//	return err
				//}
				c.Log.Errorf("faild upgrade %v", gvk)
				return err
			}
			if task.recorder != nil {
				task.recorder.Record(obj)
			}

		case bkeaddon.RemoveAddon:
			err = dr.Delete(c.Ctx, unstruct.GetName(), metav1.DeleteOptions{})
			if err != nil {
				if apierrors.IsNotFound(err) {
					c.Log.Warnf("addon obj Kind： %s, Name %s  not found, skip delete", unstruct.GetKind(), unstruct.GetName())
					continue
				}
				c.Log.Errorf("faild delete %v", gvk)
				return err
			}
		}

		// wait for obj when update or create or upgrade
		if task.Block && task.Operate != bkeaddon.RemoveAddon {
			c.Log.Infof("wait for addon obj Kind： %s, Name %s", obj.GetKind(), obj.GetName())
			if err := c.Wait(obj, task); err != nil {
				c.Log.Errorf("addon obj Kind： %s, Name %s  wait failed, err: %v", obj.GetKind(), obj.GetName(), err)
				return err
			}
		}
		if obj == nil {
			obj = &unstruct
		}
		c.Log.Infof("addon obj Kind： %s, Name %s,APIVersion %s, %s success", obj.GetKind(), obj.GetName(), obj.GetAPIVersion(), task.Operate)
	}
	return nil
}

// RenderYamlToDecoder render yaml and return decoder
func RenderYamlToDecoder(task *Task) (*yamlutil.YAMLOrJSONDecoder, error) {
	tmpFile := ""
	if !strings.Contains(task.FilePath, "noexecute") {
		b, err := os.ReadFile(task.FilePath)
		if err != nil {
			return nil, err
		}
		tmpl, err := template.New(task.Name).Funcs(templateutil.CommonFuncMap()).Parse(string(b))
		if err != nil {
			return nil, errors.Wrapf(err, "failed to parse addon %s template yaml file", task.Name)
		}
		tmpFile = fmt.Sprintf("%s/%s-%s.yaml", os.TempDir(), task.Name, uuid.NewUUID())
		f, err := os.OpenFile(tmpFile, os.O_CREATE|os.O_RDWR, 0644)
		if err != nil {
			return nil, err
		}
		defer func() {
			f.Close()
			os.Remove(tmpFile)
		}()

		if err := tmpl.Execute(f, task.Param); err != nil {
			return nil, err
		}
	} else {
		tmpFile = task.FilePath
	}

	readFile, err := os.ReadFile(tmpFile)
	if err != nil {
		return nil, err
	}

	decoder := yamlutil.NewYAMLOrJSONDecoder(bytes.NewReader(readFile), 4096)
	return decoder, nil
}

func GetUnStructListFromDecoder(decoder *yamlutil.YAMLOrJSONDecoder) ([]*unstructured.Unstructured, error) {
	var unstructList []*unstructured.Unstructured
	latestUnstructName := ""
	for {
		unstruct, _, err := UnStructYaml(decoder)
		if err != nil {
			if err == io.EOF {
				break
			}
			return nil, errors.Errorf("latest success resource %s, err: %v", latestUnstructName, err)
		}
		latestUnstructName = fmt.Sprintf("kind: %s name: %s", unstruct.GetKind(), unstruct.GetName())
		unstructList = append(unstructList, unstruct)
	}
	return unstructList, nil
}

// UnStructYaml get unstruct form yaml decoder
func UnStructYaml(decoder *yamlutil.YAMLOrJSONDecoder) (*unstructured.Unstructured, *schema.GroupVersionKind, error) {
	var rawObj runtime.RawExtension
	if err := decoder.Decode(&rawObj); err != nil {
		return nil, nil, err
	}
	obj, gvk, err := unstructured.UnstructuredJSONScheme.Decode(rawObj.Raw, nil, nil)
	if err != nil {
		return nil, nil, err
	}
	// runtime.Object convert to unstructured
	unstructuredObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
	if err != nil {
		return nil, nil, err
	}
	unstruct := &unstructured.Unstructured{Object: unstructuredObj}
	return unstruct, gvk, nil
}
